﻿namespace NTLib.Core.Shared
{
    using NTLib.Core.Threading;
    using System;
    using System.Collections.Generic;
    using NTLib.Core.Args;
    using NTLib.Core.Manipulators;
    using NTLib.Core.Cast;
    using System.Diagnostics;
    using System.Reflection;
    using NTLib.Core.Garbadge;
    using NTLib.Core.Exceptions.Shared;


    /// <summary>
    /// Property used to share a default value or a local value
    /// </summary>
    public class SharedProperty<Locker, T> : SealedBase<Locker>, ISharedValue
        where Locker : ILocker, new()
    {
        #region Fields

        private readonly ISharedValueKey _key;
        private readonly IAccessor<T> _sharedResource;
        private readonly TypeInfo _ownerType;
        private readonly Dictionary<int, IAccessor<T>> _localResources;
        private readonly Dictionary<int, WeakReference> _localWeakRefResources;

        private readonly Locker _sharedLocker = new Locker();
        private readonly SemaphoreLocker _StrongLocker;
        private readonly string _propName;

        private DateTime _lastCleanUp;

        #endregion

        #region Ctor

        /// <summary>
        /// Initializes a new instance of the <see cref="SharedProperty{Locker}" /> class.
        /// </summary>
        /// <param name="sharedAssessor">The shared assessor.</param>
        /// <param name="propName">Name of the property.</param>
        /// <param name="owner">The owner.</param>
        /// <param name="locker">The locker instance.</param>
        internal SharedProperty(IAccessor<T> sharedAssessor,
                                string propName,
                                Type owner,
                                Locker locker = default(Locker))
            : base(locker)
        {
            this._propName = propName;
            this._ownerType = owner.GetTypeInfo();
            this._sharedResource = sharedAssessor;
            this._key = new SharedValueKey(this, propName);
            this._localResources = new Dictionary<int, IAccessor<T>>();
            this._localWeakRefResources = new Dictionary<int, WeakReference>();
            this._StrongLocker = new SemaphoreLocker();
            GCHelper.GCEnded += CleanUpLocalValues;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the name.
        /// </summary>
        public string Name
        {
            get
            {
                base.CheckDisposed();
                return this._propName;
            }
        }

        /// <summary>
        /// Gets the key.
        /// </summary>
        public ISharedValueKey Key
        {
            get
            {
                base.CheckDisposed();
                return this._key;
            }
        }

        #endregion

        #region Event

        /// <summary>
        /// Occurs when the shared value changed.
        /// </summary>
        public event Action<ISharedValueKey, ValueChangedEventArgs<T>> ValueChanged;

        #endregion

        #region Methods

        /// <summary>
        /// Clears the local value.
        /// </summary>
        public void ClearLocalValue(ISharedObject obj)
        {
            CheckIfIsOwner(obj);
            using (LockerImpl.ScopeLock())
            {
                if (obj != null)
                {
                    int key = obj.GetHashCode();
                    if (this._localResources.ContainsKey(key))
                        this._localResources.Remove(key);

                    if (this._localWeakRefResources.ContainsKey(key))
                        this._localWeakRefResources.Remove(key);
                }
            }
        }

        /// <summary>
        /// Gets the shared or local value.
        /// </summary>
        public U GetValue<U>(ISharedObject obj)
        {
            Debug.Assert(typeof(U) == typeof(T));

            CheckIfIsOwner(obj);
            using (LockerImpl.ScopeLock(true))
            {
                IAccessor<T> accessor = null;
                if (!this._localResources.TryGetValue(obj.GetHashCode(), out accessor))
                    accessor = this._sharedResource;
                return CastUtility.Cast<U>(accessor.Get());
            }
        }

        /// <summary>
        /// Overrides the shared value.
        /// </summary>
        public bool OverrideSharedValue<U>(U value)
        {
            base.CheckDisposed();
            using (LockerImpl.ScopeLock())
            {
                var old = this._sharedResource.Get();
                var diff = this._sharedResource.Set(CastUtility.Cast<T>(value));
                if (diff && ValueChanged != null)
                    ValueChanged(this._key, new ValueChangedEventArgs<T>(old, this._sharedResource.Get()));
                return diff;
            }
        }

        /// <summary>
        /// Sets the local value.
        /// </summary>
        public bool SetLocalValue<U>(ISharedObject obj, U value)
        {
            CheckIfIsOwner(obj);
            using (LockerImpl.ScopeLock())
            {
                int code = obj.GetHashCode();
                IAccessor<T> t = null;
                if (!this._localResources.TryGetValue(code, out t))
                {
                    t = new SimpleAccessor<T>();
                    this._localResources.Add(obj.GetHashCode(), t);
                    this._localWeakRefResources.Add(obj.GetHashCode(), new WeakReference(obj));
                }

                var old = t.Get();
                var diff = t.Set(CastUtility.Cast<T>(value));

                if (diff && ValueChanged != null)
                    ValueChanged(this.Key, new ValueChangedEventArgs<T>(old, t.Get()));

                return diff;
            }
        }

        /// <summary>
        /// Checks the object is the owner of the property.
        /// </summary>
        protected virtual void CheckIfIsOwner(ISharedObject obj)
        {
            base.CheckDisposed();
            var info = obj.GetType().GetTypeInfo();
            if (!this._ownerType.IsAssignableFrom(info))
                throw new BadSharedPropertyOwnerException();
        }


        /// <summary>
        /// Dispose methods called whenever is the dipose origin.
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (base.IsDisposed)
                return;

            if (this._localResources != null)
                this._localResources.Clear();

            if (this._localWeakRefResources != null)
                this._localWeakRefResources.Clear();

            CleanUpLocalValues();

            GCHelper.GCEnded -= CleanUpLocalValues;

            base.Dispose(disposing);
        }

        /// <summary>
        /// Cleans up local values.
        /// </summary>
        private void CleanUpLocalValues()
        {
            if (this._StrongLocker.IsDisposed)
            {
                GCHelper.GCEnded -= CleanUpLocalValues;
                return;
            }

            using (this._StrongLocker.ScopeLock())
            {
                var now = DateTime.Now;
                if (_lastCleanUp != default(DateTime) &&
                    (now - this._lastCleanUp).TotalMinutes < 1)
                    return;
                this._lastCleanUp = now;
                using (base.LockerImpl.ScopeLock())
                {
                    List<int> toRemove = new List<int>();
                    foreach (var refObj in this._localWeakRefResources)
                    {
                        if (!refObj.Value.IsAlive)
                            toRemove.Add(refObj.Key);
                    }

                    foreach (var rm in toRemove)
                    {
                        this._localWeakRefResources.Remove(rm);
                        this._localResources.Remove(rm);
                    }
                }
            }
        }

        #endregion
    }


    /// <summary>
    /// Property used to share a default value or a local value
    /// </summary>
    [NotThreadSafe]
    public class SharedProperty<T> : SharedProperty<NoLocker, T>
    {

        /// <summary>
        /// Initializes a new instance of the <see cref="SharedProperty{Locker}" /> class.
        /// </summary>
        /// <param name="sharedAssessor">The shared assessor.</param>
        /// <param name="propName">Name of the property.</param>
        /// <param name="owner">The owner.</param>
        internal SharedProperty(IAccessor<T> sharedAssessor, string propName, Type owner)
            : base(sharedAssessor, propName, owner)
        {
        }
    }
}
